package jmine.tec.persist.schema.impl.elements;

import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedMap;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;

import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.SequenceGenerator;

import jmine.tec.persist.annotation.Alias;
import jmine.tec.persist.annotation.Comment;
import jmine.tec.persist.annotation.Comments;
import jmine.tec.persist.annotation.Index;
import jmine.tec.persist.annotation.Indexes;
import jmine.tec.persist.annotation.Tablespace;
import jmine.tec.persist.annotation.UniqueConstraint;
import jmine.tec.persist.annotation.UniqueConstraints;
import jmine.tec.persist.annotation.View;
import jmine.tec.persist.schema.api.AbstractProcessingSchemaElement;
import jmine.tec.persist.schema.api.ProcessingSchemaElement;
import jmine.tec.persist.schema.api.SqlDialect;
import jmine.tec.persist.schema.impl.elements.type.AllowedValue;
import jmine.tec.utils.Tuple;
import jmine.tec.utils.reflection.AnnotationUtils;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.lang.builder.CompareToBuilder;
import org.hibernate.cfg.Configuration;
import org.hibernate.mapping.Collection;
import org.hibernate.mapping.Column;
import org.hibernate.mapping.Component;
import org.hibernate.mapping.IndexedCollection;
import org.hibernate.mapping.JoinedSubclass;
import org.hibernate.mapping.OneToMany;
import org.hibernate.mapping.PersistentClass;
import org.hibernate.mapping.Property;
import org.hibernate.mapping.Table;
import org.hibernate.mapping.ToOne;
import org.hibernate.mapping.UnionSubclass;
import org.hibernate.mapping.Value;
import org.springframework.core.BridgeMethodResolver;

import bancosys.tec.component.Documentation;

/**
 * SchemaElement representando uma tabela.
 * 
 * @author lundberg
 */
public class TableElement extends AbstractProcessingSchemaElement<TableProcessingBean> implements Comparable<TableElement> {

    private static final int MAX_ALIAS_LENGTH = 6;

    private final String name;

    private String tablespace;

    private String alias;

    private final SortedMap<String, ColumnElement> columns;

    private final List<AlternateKeyElement> alternateKeys;

    private final List<ForeignKeyElement> foreignKeys;

    private final Set<CheckConstraintElement> checkConstraints;

    private final SortedSet<IndexElement> indexes;

    private final SortedSet<CommentElement> comments;

    private PrimaryKeyElement primaryKey;

    private Schema schema;

    private List<String> warnings = new ArrayList<String>();

    /**
     * Construtor
     * 
     * @param parent parent
     * @param name name
     */
    public TableElement(ProcessingSchemaElement<?> parent, String name) {
        super(parent);
        this.name = name;
        this.columns = new TreeMap<String, ColumnElement>();
        this.alternateKeys = new LinkedList<AlternateKeyElement>();
        this.comments = new TreeSet<CommentElement>();
        this.foreignKeys = new LinkedList<ForeignKeyElement>();
        this.checkConstraints = new HashSet<CheckConstraintElement>();
        this.indexes = new TreeSet<IndexElement>();
    }

    /**
     * {@inheritDoc}
     */
    public List<String> createStatements(SqlDialect dialect) {
        List<String> statements = new LinkedList<String>();
        statements.addAll(dialect.renderTable(this.tablespace, this.name, this.columns.values()));
        if (this.primaryKey != null) {
            statements.addAll(this.primaryKey.createStatements(dialect));
        }
        Collections.sort(this.alternateKeys);
        statements.addAll(dialect.renderElements(this.alternateKeys));
        statements.addAll(dialect.renderElements(this.comments));
        return statements;
    }

    /**
     * Processa a tabela, incluindo todas as colunas contidas, processamento de índices, comentários, check constraints, sequences, tipos,
     * tabelas e colunas definidas em collections e views.
     * 
     * @param bean dados necessários para o processamento
     */
    public void process(TableProcessingBean bean) {
        PersistentClass metadata = bean.getPersistentClass();
        Class<?> klass = metadata.getMappedClass();
        if (klass.isAnnotationPresent(View.class)) {
            this.schema.removeTable(this.name);
        } else {
            this.alias = this.findAlias(klass);
            this.checkAlias();
            this.processIndexes(klass);
            this.processTablespace(klass);
            this.processProperties(bean.getConfiguration(), metadata);
            this.processUniqueConstraints(klass);
            this.processSequences(klass);
            this.processDiscriminator(metadata);
            this.processComments(metadata, klass);
        }
    }

    /**
     * Processa as anotações relativas a unique constraints presentes no elemento anotado.
     * 
     * @param annotated annotated
     */
    private void processUniqueConstraints(AnnotatedElement annotated) {
        if (annotated.isAnnotationPresent(UniqueConstraint.class)) {
            this.processUniqueConstraint(annotated.getAnnotation(UniqueConstraint.class), this);
        }
        if (annotated.isAnnotationPresent(UniqueConstraints.class)) {
            for (UniqueConstraint constraint : annotated.getAnnotation(UniqueConstraints.class).value()) {
                this.processUniqueConstraint(constraint, this);
            }
        }
    }

    /**
     * Adiciona a ak correspondente à anotação
     * 
     * @param annotation annotation
     * @param tableElement tabela referente à AK
     */
    private void processUniqueConstraint(UniqueConstraint annotation, TableElement tableElement) {
        AlternateKeyElement ak = new AlternateKeyElement(tableElement);
        ak.setName(this.createUniqueConstraintName(tableElement, annotation));
        if (annotation.columns().length == 0) {
            throw new IllegalArgumentException(String.format("Não é possível inferir as colunas da unique constraint %s na posição dada.",
                    annotation.name()));
        }
        for (String column : annotation.columns()) {
            ak.addColumn(column);
        }
        tableElement.addAlternateKey(ak);
    }

    /**
     * Processa a anotação Tablespace
     * 
     * @param annotated annotated
     */
    private void processTablespace(AnnotatedElement annotated) {
        if (annotated.isAnnotationPresent(Tablespace.class)) {
            this.tablespace = annotated.getAnnotation(Tablespace.class).name();
        }
    }

    /**
     * Efetua o processamento reference ao discriminator do mapeamento, caso exista.
     * 
     * @param metadata mapeamento criado pelo hibernate
     */
    private void processDiscriminator(PersistentClass metadata) {
        Value discriminator = metadata.getDiscriminator();
        if (discriminator != null) {
            Iterator<?> it = discriminator.getColumnIterator();
            while (it.hasNext()) {
                Column col = (Column) it.next();
                this.findColumn(col.getName()).processDiscriminator(metadata);
            }
        }
    }

    /**
     * Processa as anotações de índices definidas na classe (não inclui anotações em métodos)
     * 
     * @param klass klass
     */
    private void processIndexes(Class<?> klass) {
        for (Index index : this.getAnnotatedElementIndexes(klass)) {
            this.addIndex(new IndexElement(this, index, this));
        }
    }

    /**
     * Processa a anotação SequenceGenerator, caso esteja presente na classe, para que a sequence relacionada possa ler seu valor inicial
     * correto.
     * 
     * @param klass klass
     */
    private void processSequences(Class<?> klass) {
        if (klass.isAnnotationPresent(SequenceGenerator.class)) {
            SequenceGenerator generator = klass.getAnnotation(SequenceGenerator.class);
            final SequenceElement sequence = this.schema.findSequence(generator.name());
            sequence.process(generator);
            sequence.setTableName(this.name);
        }
    }

    /**
     * Processa a chave primária da tabela, caso exista, para que um nome apropriado seja definido.
     */
    private void processPrimaryKey() {
        if (this.primaryKey != null) {
            this.primaryKey.process(this);
        }
    }

    /**
     * Processa as propriedades da classe mapeada
     * 
     * @param configuration configuration
     * @param metadata mapeamento criado pelo hibernate
     */
    private void processProperties(Configuration configuration, PersistentClass metadata) {
        Property identifier = metadata.getIdentifierProperty();

        boolean tablePerClass = metadata instanceof UnionSubclass;
        boolean joined = metadata instanceof JoinedSubclass;
        if (metadata.isInherited() && !tablePerClass) {
            Method getter = this.getPropertyGetter(metadata.getRootClass().getMappedClass(), identifier);
            Column column;
            if (joined) {
                column = (Column) ((JoinedSubclass) metadata).getKey().getColumnIterator().next();
            } else {
                column = (Column) identifier.getColumnIterator().next();
            }
            this.findColumn(column.getName()).processInheritedIdentifier(new ColumnProcessingBean(getter, column));
        } else {
            this.processProperty(configuration, metadata, identifier);
        }
        Iterator<?> it = tablePerClass ? metadata.getReferenceablePropertyIterator() : metadata.getPropertyIterator();
        while (it.hasNext()) {
            this.processProperty(configuration, metadata, (Property) it.next());
        }
    }

    /**
     * Processa os comentários anotados no elemento anotado
     * 
     * @param metadata mapeamento
     * @param annotated elemento anotado
     */
    private void processComments(PersistentClass metadata, AnnotatedElement annotated) {
        if (annotated.isAnnotationPresent(Comment.class)) {
            this.processComment(metadata, annotated.getAnnotation(Comment.class));
        }
        if (annotated.isAnnotationPresent(Comments.class)) {
            for (Comment comment : annotated.getAnnotation(Comments.class).value()) {
                this.processComment(metadata, comment);
            }
        }
        if (metadata != null && annotated.isAnnotationPresent(Documentation.class)) {
            this.processDocumentation(annotated.getAnnotation(Documentation.class));
        }
    }

    /**
     * Cria um elemento de comentário para a tabela referente a uma anotação de Documentation
     * 
     * @param annotation anotação
     */
    private void processDocumentation(Documentation annotation) {
        this.addComment(new CommentElement(this.name, annotation.value()));
    }

    /**
     * Cria o elemento de comentário apropriado para a tabela ou para alguma coluna de discriminator
     * 
     * @param metadata mapeamento
     * @param annotation anotação
     */
    private void processComment(PersistentClass metadata, Comment annotation) {
        if (annotation.column().length() == 0) {
            this.addComment(new CommentElement(this.name, annotation.value()));
        } else if (metadata != null) {
            if (metadata.isInherited() && this.isPkColumn(annotation.column())) {
                this.addComment(new CommentElement(this.name, annotation.column(), annotation.value()));
            }
            if (metadata.getDiscriminator() != null) {
                Iterator<?> it = metadata.getDiscriminator().getColumnIterator();
                while (it.hasNext()) {
                    Column column = (Column) it.next();
                    if (column.getName().equals(annotation.column())) {
                        this.addComment(new CommentElement(this.name, column.getName(), annotation.value()));
                    }
                }
            }
        }
    }

    /**
     * Verifica se a coluna com o nome dado pertence à pk
     * 
     * @param column column
     * @return boolean
     */
    private boolean isPkColumn(String column) {
        if (this.primaryKey != null) {
            for (String pkColumn : this.primaryKey.getColumns()) {
                if (pkColumn.equals(column)) {
                    return true;
                }
            }
        }
        return false;
    }

    /**
     * Processa uma propriedade do mapeamento
     * 
     * @param configuration configuration
     * @param metadata metadata
     * @param property property
     */
    private void processProperty(Configuration configuration, PersistentClass metadata, Property property) {
        this.processPropertyAnnotations(metadata.getMappedClass(), Schema.getTableName(metadata.getTable(), configuration), property,
                this.schema);
        if (property.getValue().isSimpleValue() || OneToMany.class.isAssignableFrom(property.getValue().getClass())) {
            this.processSimpleValue(configuration, property);
        } else if (Collection.class.isAssignableFrom(property.getValue().getClass())) {
            this.processCollection(configuration, property);
        } else {
            throw new IllegalStateException();
        }
    }

    /**
     * Processa uma anotações references a comentários em uma propriedade
     * 
     * @param mappedClass mappedClass
     * @param table table
     * @param property property
     * @param parent parent
     */
    private void processPropertyAnnotations(Class<?> mappedClass, String table, Property property, ProcessingSchemaElement<?> parent) {
        if (property.isBackRef()) {
            return;
        }
        Method getter = this.getPropertyGetter(mappedClass, property);
        if (getter != null) {
            if (getter.isAnnotationPresent(Comment.class)) {
                this.processPropertyComment(getter.getAnnotation(Comment.class));
            }
            if (getter.isAnnotationPresent(Comments.class)) {
                for (Comment comment : getter.getAnnotation(Comments.class).value()) {
                    this.processPropertyComment(comment);
                }
            }
        }
        boolean isCollection = property.getValue() instanceof Collection;
        String realTable = isCollection ? ((Collection) property.getValue()).getCollectionTable().getName() : table;
        TableElement realTableElement = this.getSchema().findTable(realTable);

        if (isCollection) {
            Method collectionGetter = this.getPropertyGetter(property.getPersistentClass().getMappedClass(), property);
            if (collectionGetter != null) {
                if (collectionGetter.isAnnotationPresent(JoinTable.class)) {
                    if (collectionGetter.isAnnotationPresent(Alias.class)) {
                        realTableElement.alias = collectionGetter.getAnnotation(Alias.class).value();
                        realTableElement.checkAlias();
                    }
                    if (collectionGetter.isAnnotationPresent(Tablespace.class)) {
                        realTableElement.tablespace = collectionGetter.getAnnotation(Tablespace.class).name();
                    }
                } else if (collectionGetter.isAnnotationPresent(JoinColumn.class)) {
                    Class<?> returnedClass = ((Collection) property.getValue()).getElement().getType().getReturnedClass();
                    if (returnedClass.isAnnotationPresent(Alias.class)) {
                        realTableElement.alias = returnedClass.getAnnotation(Alias.class).value();
                        realTableElement.checkAlias();
                    }
                    if (returnedClass.isAnnotationPresent(Tablespace.class)) {
                        realTableElement.tablespace = returnedClass.getAnnotation(Tablespace.class).name();
                    }
                }
            }
        }

        if (getter != null) {
            if (getter.isAnnotationPresent(Documentation.class)) {
                if (isCollection) {
                    Collection collection = (Collection) property.getValue();
                    if (!collection.isInverse()) {
                        Column column = (Column) this.getPropertyColumnIterator(property).next();
                        TableElement tableElement = this.getSchema().findTable(collection.getKey().getTable().getName());
                        Documentation ann = getter.getAnnotation(Documentation.class);
                        tableElement.addComment(new CommentElement(tableElement.getName(), column.getName(), ann.value()));
                    }
                    property.getValue();
                } else {
                    this.processPropertyDocumentation(property, realTableElement, getter.getAnnotation(Documentation.class));
                }
            }
            this.processUniqueConstraints(property, getter, realTableElement);
            for (Index index : this.getAnnotatedElementIndexes(getter)) {
                realTableElement.addIndex(new IndexElement(realTableElement, this.getPropertyColumnIterator(property), index, this));
            }
        }
    }

    /**
     * @param property property
     * @param getter getter
     * @param realTableElement realTableElement
     */
    private void processUniqueConstraints(Property property, Method getter, TableElement realTableElement) {
        if (getter.isAnnotationPresent(UniqueConstraint.class)) {
            this.processPropertyUniqueConstraint(property, realTableElement, getter.getAnnotation(UniqueConstraint.class));
        }

        if (getter.isAnnotationPresent(UniqueConstraints.class)) {
            for (UniqueConstraint constraint : getter.getAnnotation(UniqueConstraints.class).value()) {
                this.processPropertyUniqueConstraint(property, realTableElement, constraint);
            }
        }
    }

    /**
     * Obtém o getter anotado da propriedade
     * 
     * @param mappedClass mappedClass
     * @param property property
     * @return Method
     */
    private Method getPropertyGetter(Class<?> mappedClass, Property property) {
        return BridgeMethodResolver.findBridgedMethod(property.getGetter(mappedClass).getMethod());
    }

    /**
     * Obtém o iterador de colunas da propriedade. Refere-se às colunas que dizem respeito à anotações de colunas.
     * 
     * @param property property
     * @return Iterator
     */
    private Iterator<?> getPropertyColumnIterator(Property property) {
        boolean isCollection = property.getValue() instanceof Collection;
        return isCollection ? ((Collection) property.getValue()).getKey().getColumnIterator() : property.getColumnIterator();
    }

    /**
     * Processa uma anotação de unique constraint encontrada em uma propriedade
     * 
     * @param property property
     * @param table table
     * @param annotation annotation
     */
    private void processPropertyUniqueConstraint(Property property, TableElement table, UniqueConstraint annotation) {
        List<String> uniqueColumns = new ArrayList<String>();
        if (annotation.columns().length == 0) {
            Iterator<?> columnIterator = this.getPropertyColumnIterator(property);
            while (columnIterator.hasNext()) {
                uniqueColumns.add(((Column) columnIterator.next()).getName());
            }
        } else {
            uniqueColumns.addAll(Arrays.asList(annotation.columns()));
        }
        for (AlternateKeyElement c : table.alternateKeys) {
            if (c.getColumns().equals(uniqueColumns)) {
                c.setName(this.createUniqueConstraintName(table, annotation));
                return;
            }
        }
        AlternateKeyElement ak = new AlternateKeyElement(table);
        ak.setName(this.createUniqueConstraintName(table, annotation));
        ak.addColumns(uniqueColumns);
        table.addAlternateKey(ak);
    }

    /**
     * Cria o nome da unique constraint
     * 
     * @param table table
     * @param annotation annotation
     * @return string
     */
    private String createUniqueConstraintName(TableElement table, UniqueConstraint annotation) {
        if (StringUtils.isNotBlank(annotation.suffix())) {
            return String.format("AK_%s_%s", table.alias, annotation.suffix());
        } else {
            return annotation.name();
        }
    }

    /**
     * Processa uma anotação de Documentation colocada em uma propriedade.
     * 
     * @param property property
     * @param table table
     * @param annotation annotation
     */
    private void processPropertyDocumentation(Property property, TableElement table, Documentation annotation) {
        if (property.getColumnSpan() != 1) {
            this.warn(String.format(
                    "Não foi possível determinar a coluna que deveria receber o comentário na propriedade %s na tabela %s.", property,
                    table.getName()));
        } else {
            Column column = (Column) property.getColumnIterator().next();
            table.addComment(new CommentElement(table.getName(), column.getName(), annotation.value()));
        }
    }

    /**
     * Processa uma anotação de Comment colocada em uma propriedade.
     * 
     * @param annotation annotation
     */
    private void processPropertyComment(Comment annotation) {
        TableElement table = this.getSchema().findTable(annotation.table());
        table.addComment(new CommentElement(annotation.table(), annotation.column(), annotation.value()));
    }

    /**
     * Processa uma propriedade reference a uma collection
     * 
     * @param configuration configuration
     * @param property property
     */
    private void processCollection(Configuration configuration, Property property) {
        Collection collection = (Collection) property.getValue();
        if (collection.isInverse()) {
            // Mapped by, não é necessário processar aqui
            return;
        }
        Table table = collection.getCollectionTable();
        if (table.hasDenormalizedTables() || Schema.getTableName(table, configuration).equals(this.name)) {
            Method collectionGetter = this.getPropertyGetter(property.getPersistentClass().getMappedClass(), property);
            if (collection.getKey().getColumnSpan() == 1) {
                Column column = (Column) collection.getKey().getColumnIterator().next();
                Method ownerIdentifierGetter =
                        this.getPropertyGetter(collection.getOwner().getMappedClass(), collection.getOwner().getIdentifierProperty());
                this.findColumn(column.getName()).process(new ColumnProcessingBean(ownerIdentifierGetter, column));
            }
            // condição evita reprocessar o ID da entidade referenciada pelo OneToMany
            if (!(collection.getElement() instanceof OneToMany && ((OneToMany) collection.getElement()).getAssociatedClass().getTable()
                    .getName().equals(this.name))) {
                if (collection.getElement() instanceof Component) {
                    Component component = (Component) collection.getElement();
                    Iterator<?> compPropIt = component.getPropertyIterator();
                    while (compPropIt.hasNext()) {
                        Property prop = (Property) compPropIt.next();
                        this.processPropertyAnnotations(component.getComponentClass(), table.getName(), prop, this.schema);
                        this.processSimpleValue(configuration, component.getComponentClass(), prop);
                    }
                } else if (collection.getElement().getColumnSpan() == 1) {
                    if (collection.getElement() instanceof ToOne) {
                        ToOne toOne = (ToOne) collection.getElement();
                        PersistentClass referenced = configuration.getClassMapping(toOne.getReferencedEntityName());
                        Property identifier = referenced.getIdentifierProperty();
                        Method identifierGetter = this.getPropertyGetter(referenced.getMappedClass(), identifier);
                        Column column = (Column) collection.getElement().getColumnIterator().next();
                        this.findColumn(column.getName()).process(new ColumnProcessingBean(identifierGetter, column));
                    } else if (collectionGetter.isAnnotationPresent(javax.persistence.Column.class)) {
                        Column column = (Column) collection.getElement().getColumnIterator().next();
                        this.findColumn(column.getName()).process(new ColumnProcessingBean(collectionGetter, column));
                    } else {
                        // (lundberg) Possivelmente OK, a não ser que outro processamento venha a ser necessário
                    }
                } else {
                    // (lundberg) Possivelmente OK, a não ser que outro processamento venha a ser necessário
                }
            }
            if (collection.isIndexed()) {
                IndexedCollection idxCol = (IndexedCollection) collection;
                if (idxCol.getIndex() instanceof Component) {
                    Component component = (Component) idxCol.getIndex();
                    Iterator<?> compPropIt = component.getPropertyIterator();
                    while (compPropIt.hasNext()) {
                        Property prop = (Property) compPropIt.next();
                        this.processPropertyAnnotations(component.getComponentClass(), table.getName(), prop, this.schema);
                        this.processSimpleValue(configuration, component.getComponentClass(), prop);
                    }
                } else if (idxCol.getIndex().getColumnSpan() == 1 && idxCol.getIndex() instanceof ToOne) {
                    ToOne toOne = (ToOne) idxCol.getIndex();
                    PersistentClass referenced = configuration.getClassMapping(toOne.getReferencedEntityName());
                    Property identifier = referenced.getIdentifierProperty();
                    Method identifierGetter = this.getPropertyGetter(referenced.getMappedClass(), identifier);
                    Column column = (Column) collection.getElement().getColumnIterator().next();
                    this.findColumn(column.getName()).process(new ColumnProcessingBean(identifierGetter, column));
                } else {
                    // (lundberg) Possivelmente OK, a não ser que outro processamento venha a ser necessário
                }
            }
        } else {
            this.schema.findTable(table.getName()).processCollection(configuration, property);
        }
    }

    /**
     * Processa uma propriedade "simples", que não são coleções ou OneToMany
     * 
     * @param configuration configuration
     * @param property property
     */
    private void processSimpleValue(Configuration configuration, Property property) {
        this.processSimpleValue(configuration, property.getPersistentClass().getMappedClass(), property);
    }

    /**
     * Processa uma propriedade "simples", que não são coleções ou OneToMany
     * 
     * @param configuration configuration
     * @param mappedClass mappedClass
     * @param property property
     */
    private void processSimpleValue(Configuration configuration, Class<?> mappedClass, Property property) {
        if (property.isBackRef()) {
            return;
        }
        Table table = property.getValue().getTable();

        if (table.hasDenormalizedTables() || Schema.getTableName(table, configuration).equals(this.name)) {
            if (property.isComposite()) {
                Component component = (Component) property.getValue();
                Iterator<?> compIt = component.getPropertyIterator();
                while (compIt.hasNext()) {
                    Property prop = (Property) compIt.next();
                    this.processPropertyAnnotations(component.getComponentClass(), table.getName(), prop, this.schema);
                    this.processSimpleValue(configuration, component.getComponentClass(), prop);
                }
            } else if (property.getColumnSpan() == 1) {
                Method getter = this.getPropertyGetter(mappedClass, property);
                if (getter != null) {
                    if (property.getValue() instanceof ToOne) {
                        ToOne toOne = (ToOne) property.getValue();
                        PersistentClass referenced = configuration.getClassMapping(toOne.getReferencedEntityName());
                        Property identifier = referenced.getIdentifierProperty();
                        Method identifierGetter = this.getPropertyGetter(referenced.getMappedClass(), identifier);
                        Column column = (Column) property.getValue().getColumnIterator().next();
                        this.findColumn(column.getName()).process(new ColumnProcessingBean(identifierGetter, column));
                        this.processUniqueConstraints(property, getter, this);
                    } else if (getter.isAnnotationPresent(javax.persistence.Column.class)) {
                        Column column = (Column) property.getValue().getColumnIterator().next();
                        this.findColumn(column.getName()).process(new ColumnProcessingBean(getter, column));
                        this.processUniqueConstraints(property, getter, this);
                    } else {
                        // (lundberg) Possivelmente OK, a não ser que outro processamento venha a ser necessário
                    }
                }
            }
        } else {
            this.schema.findTable(Schema.getTableName(table, configuration)).processSimpleValue(configuration, property);
        }
    }

    /**
     * Efetua a segunda rodada de processamento, com passos que devem ser feitos após o processamento de todas as tabelas.<br/>
     * Processa PK's, AK's, FK's, check constraints e índices.
     */
    public void processSecondPass() {
        this.processPrimaryKey();
        this.processAlternateKeys();
        this.processForeignKeys();
        for (ColumnElement column : this.columns.values()) {
            this.checkConstraints.addAll(column.createCheckConstraints(this));
        }
        for (CheckConstraintElement check : new TreeSet<CheckConstraintElement>(this.checkConstraints)) {
            check.process(this);
        }
        for (IndexElement index : new TreeSet<IndexElement>(this.indexes)) {
            index.process(this);
        }
        this.appendAllowedValueDescriptions();
        this.checkComments();
    }

    /**
     * Adiciona novos valores
     */
    private void appendAllowedValueDescriptions() {
        for (CommentElement comment : this.comments) {
            this.appendAllowedValueDescriptions(comment);
        }
    }

    /**
     * Adiciona ao comentário a descrição dos valores permitidos.
     * 
     * @param comment comment
     */
    private void appendAllowedValueDescriptions(CommentElement comment) {
        if (comment.isTableComment()) {
            return;
        }
        ColumnElement column = this.columns.get(comment.getColumn());
        if (column == null) {
            this.warn("Tentativa inválida de adicionar comentário em uma coluna que não existe: " + this.name + "." + comment.getColumn());
            return;
        }
        if (column.getAllowedValues().isEmpty()) {
            return;
        }
        List<String> values = new ArrayList<String>();
        for (AllowedValue allowedValue : column.getAllowedValues()) {
            if (comment.getComment().contains(allowedValue.getValue())) {
                return;
            }
            if (allowedValue.getDescription() != null) {
                values.add(String.format("(%s) %s", allowedValue.getValue(), allowedValue.getDescription()));
            }
        }
        comment.append(String.format(" %s.", StringUtils.join(values, ", ")));
    }

    /**
     * Verifica se todos os comentários foram informados, registrando warnings caso contrário
     */
    private void checkComments() {
        boolean tableCommentFound = false;
        Set<String> cols = new HashSet<String>(this.columns.keySet());
        for (CommentElement comment : this.comments) {
            if (comment.isTableComment()) {
                tableCommentFound = true;
            } else {
                cols.remove(comment.getColumn());
            }
        }
        if (!tableCommentFound) {
            this.warn("Comentário não encontrado para tabela " + this.name);
        }
        for (String column : cols) {
            this.warn(String.format("Comentário não encontrado para coluna %s.%s", this.name, column));
        }
    }

    /**
     * Processa alternate keys
     */
    private void processAlternateKeys() {
        Collections.sort(this.alternateKeys);
        for (AlternateKeyElement ak : this.alternateKeys) {
            ak.process(this);
        }
    }

    /**
     * Processa foreign keys
     */
    private void processForeignKeys() {
        Set<Set<String>> removedKeys = new HashSet<Set<String>>();
        Map<Set<String>, ForeignKeyElement> fkMap = new HashMap<Set<String>, ForeignKeyElement>();
        for (ForeignKeyElement fk : new ArrayList<ForeignKeyElement>(this.foreignKeys)) {
            fk.process(this);
            Set<String> key = new HashSet<String>(Arrays.asList(fk.getReferencingColumns()));
            if (fkMap.containsKey(key)) {
                if (fkMap.get(key).hasSameTarget(fk)) {
                    if (fk.hasRawName()) {
                        this.foreignKeys.remove(fk);
                    } else {
                        this.foreignKeys.remove(fkMap.get(key));
                        fkMap.put(key, fk);
                    }
                } else {
                    removedKeys.add(key);
                }
            } else {
                fkMap.put(key, fk);
            }
        }
        if (!removedKeys.isEmpty()) {
            Iterator<ForeignKeyElement> removedKeysIterator = this.foreignKeys.iterator();
            while (removedKeysIterator.hasNext()) {
                ForeignKeyElement fk = removedKeysIterator.next();
                Set<String> referencing = new HashSet<String>(Arrays.asList(fk.getReferencingColumns()));
                if (removedKeys.contains(referencing)) {
                    this.warn(String.format("FK removida pois coluna aponta para 2 tabelas distintas: %s ", fk.toString()));
                    removedKeysIterator.remove();
                }
            }
        }
        Collections.sort(this.foreignKeys, new Comparator<ForeignKeyElement>() {
            public int compare(ForeignKeyElement o1, ForeignKeyElement o2) {
                return new CompareToBuilder().append(o1.getReferencingColumns(), o2.getReferencingColumns()).toComparison();
            }
        });
        for (ForeignKeyElement fk : this.foreignKeys) {
            fk.processSecondPass(this);
        }
    }

    /**
     * Encontra o alias da classe.
     * 
     * @param klass klass
     * @return String
     */
    private String findAlias(Class<?> klass) {
        Tuple<Class<?>, Alias> t = AnnotationUtils.findClassAndAnnotationWithAnnotation(klass, Alias.class);
        if (t != null) {
            return t.tail().value();
        } else {
            return null;
        }
    }

    /**
     * Verifica se o alias está de acordo com o padrão, loga um warning caso não esteja.
     */
    private void checkAlias() {
        if (this.alias == null) {
            this.warn(String.format("Alias não definido para tabela %s.", this.name));
        } else if (this.alias.length() > MAX_ALIAS_LENGTH) {
            this.warn(String.format("Alias definido para tabela %s possui mais que 6 letras: %s", this.name, this.alias));
        }
    }

    /**
     * Encontra todas anotações Index presentes no elemento.
     * 
     * @param ae elemento anotado
     * @return lista de Index
     */
    private List<Index> getAnnotatedElementIndexes(AnnotatedElement ae) {
        List<Index> ixList = new ArrayList<Index>();
        if (ae.isAnnotationPresent(Index.class)) {
            ixList.add(ae.getAnnotation(Index.class));
        }
        if (ae.isAnnotationPresent(Indexes.class)) {
            ixList.addAll(Arrays.asList(ae.getAnnotation(Indexes.class).value()));
        }
        return ixList;
    }

    /**
     * Obtém o nome da tabela
     * 
     * @return nome da tabela
     */
    public String getName() {
        return this.name;
    }

    /**
     * Obtém o alias da tabela, um nome "truncado" com 6 letras ou menos, único, definido através da anotação Alias
     * 
     * @return alias
     */
    public String getAlias() {
        return this.alias;
    }

    /**
     * Encontra a coluna com o nome dado
     * 
     * @param columnName nome da coluna sendo buscada
     * @return ColumnElement
     */
    public ColumnElement findColumn(String columnName) {
        if (!this.columns.containsKey(columnName)) {
            throw new IllegalStateException();
        } else {
            return this.columns.get(columnName);
        }
    }

    /**
     * Define a chave primária da tabela
     * 
     * @param primaryKey chave primária
     */
    public void setPrimaryKey(PrimaryKeyElement primaryKey) {
        primaryKey.setTable(this);
        this.primaryKey = primaryKey;
    }

    /**
     * Define o schema ao qual a tabela pertence
     * 
     * @param schema schema
     */
    public void setSchema(Schema schema) {
        this.schema = schema;
    }

    /**
     * Adiciona uma nova chave alternativa
     * 
     * @param ak chave alternativa
     */
    public void addAlternateKey(AlternateKeyElement ak) {
        if (ak.getName() != null && this.existsAk(ak.getName())) {
            this.warn(String.format("Index duplicado para tabela %s.", ak.getName()));
            return;
        }
        ak.setTable(this.name);
        this.alternateKeys.add(ak);
    }

    /**
     * Adiciona uma nova chave estrangeira
     * 
     * @param fk chave estrangeira
     */
    public void addForeignKey(ForeignKeyElement fk) {
        this.foreignKeys.add(fk);
    }

    /**
     * Adiciona uma nova check constraint
     * 
     * @param checkConstraint check constraint
     */
    public void addCheckConstraint(CheckConstraintElement checkConstraint) {
        this.checkConstraints.add(checkConstraint);
    }

    /**
     * Adiciona uma nova coluna
     * 
     * @param column coluna
     */
    public void addColumn(ColumnElement column) {
        column.setTable(this);
        this.columns.put(column.getName(), column);
    }

    /**
     * Adiciona um comentário à tabela
     * 
     * @param comment comment
     */
    public void addComment(CommentElement comment) {
        if (this.comments.contains(comment)) {
            CommentElement original = this.comments.tailSet(comment).first();
            if (!original.getComment().equals(comment.getComment())) {
                if (comment.isTableComment()) {
                    this.warn(String.format("Comment duplicado para tabela %s. [%s] e [%s].", comment.getTable(), original.getComment(),
                            comment.getComment()));
                } else {
                    this.warn(String.format("Comment duplicado para coluna %s.%s. [%s] e [%s].", comment.getTable(), comment.getColumn(),
                            original.getComment(), comment.getComment()));
                }
            }
        } else {
            this.comments.add(comment);
        }
    }

    /**
     * Adiciona um novo índice
     * 
     * @param index índice
     */
    public void addIndex(IndexElement index) {
        if (!this.indexes.contains(index)) {
            this.indexes.add(index);
        }
    }

    /**
     * {@inheritDoc}
     */
    public int compareTo(TableElement o) {
        return this.name.compareTo(o.name);
    }

    /**
     * Cria um nome de chave alternativa que não esteja sendo usado.
     * 
     * @return nome de chave alternativa
     */
    public String generateAkName() {
        int i = 0;
        while (this.existsAk(String.format("AK_%s_%s", this.alias, i))) {
            i++;
        }
        return String.format("AK_%s_%s", this.alias, i);
    }

    /**
     * Verifica se existe uma chave alternativa com o nome dado
     * 
     * @param akName nome
     * @return boolean
     */
    private boolean existsAk(String akName) {
        for (AlternateKeyElement ak : this.alternateKeys) {
            if (akName.equals(ak.getName())) {
                return true;
            }
        }
        return false;
    }

    /**
     * Obtém a lista de chaves estrangeiras da tabela
     * 
     * @return tabela
     */
    public List<ForeignKeyElement> getForeignKeys() {
        return this.foreignKeys;
    }

    /**
     * Obtém o schema que contém a tabela.
     * 
     * @return schema
     */
    public Schema getSchema() {
        return this.schema;
    }

    /**
     * Obtém a chave primária da tabela
     * 
     * @return chave primária
     */
    public PrimaryKeyElement getPrimaryKey() {
        return this.primaryKey;
    }

    /**
     * Obtém o conjunto de check constraints da tabela
     * 
     * @return set de check constraints
     */
    public Set<CheckConstraintElement> getCheckConstraints() {
        return this.checkConstraints;
    }

    /**
     * Obtém a lista de índices da tabela
     * 
     * @return lista de índices
     */
    public SortedSet<IndexElement> getIndexes() {
        return new TreeSet<IndexElement>(this.indexes);
    }

    /**
     * @return the tablespace
     */
    public String getTablespace() {
        return this.tablespace;
    }

    /**
     * @return the columns
     */
    public SortedMap<String, ColumnElement> getColumns() {
        return this.columns;
    }

    /**
     * @return the alternateKeys
     */
    public List<AlternateKeyElement> getAlternateKeys() {
        return this.alternateKeys;
    }

    /**
     * @return the comments
     */
    public SortedSet<CommentElement> getComments() {
        return this.comments;
    }

    /**
     * @return the warnings
     */
    public List<String> getWarnings() {
        return this.warnings;
    }
}
